C语言中实现多态
先看下面这段代码:
|
|
结果如下:
|
|
在上面的代码中:
|
|
这一行代码将_B
类(子类)对象的地址转换为_A
类指针,此时p指针实际指向的是_B
类(子类)对象的_a_
成员,因此调用的是_a_
成员的fun()函数。
如果我们将上面代码中_B
类(子类)的代码顺序变化一下:
|
|
此时运行会出错。
C++中的多态
看下面这个例子:
|
|
这段代码实际的输出为:
|
|
C++编译器在编译的时候,要确定每个对象调用的函数的地址,这称为早期绑定,当我们将fish类的对象fh的地址赋给pAn时,C++编译器进行了强制类型转换,此时C++编译器会认为pAn保存的就是animal对象的地址,当在main()函数中执行pAn->breathe()
时,调用的自然就是animal对象的breathe()
函数。
fish对象内存模型如下:
我们构造fish类的对象时,首先要调用animal类的构造函数去构造animal类的对象,然后才调用fish类的构造函数完成自身部分的构造,从而拼接出一个完整的fish对象。当我们将fish类的对象转换为animal类型时,该对象就被认为是原对象整个内存模型的上半部分,也就是图中animal对象所占内存,那么当我们利用类型转换后的对象指针去调用它的方法时,自然也就是调用它所在内存中的方法。
但是上面这种输出并不是我们想要的,我们想要的输出应当是fish bubble,这个时候就需要使用虚函数了。
前面输出的结果是因为编译器在编译的时候就已经确定了对象调用的函数的地址,要解决这个就要使用延迟绑定的技术,当编译器使用延迟绑定时,就会在运行时再去确定对象的类型以及正确的调用函数,我们对上面的代码进行修改:
|
|
这样输出的结果就是fish bubble,为什么将breathe()
声明为virtual后就会输出fish bubble呢?
编译器在编译的时候发现animal类中有虚函数,此时编译器会为每个包含虚函数的类创建一个虚表,该表是一个一维数组,在这个数组中存放每个虚函数的地址,对于上面的例子,animal类和fish类都包含一个虚函数breathe(),因此编译器会为这两个类都创建一个虚表,如下所示:
编译器还会为每个类的对象提供一个虚表指针,这个指针指向了所属类的虚表,在程序运行时根据对象的类型去初始化虚表指针,从而在调用虚函数时找到正确的函数,在上面的程序中,由于pAn实际指向的对象类型是fish,因此虚表指针指向的fish类的虚函数表,当调用pAn->breathe()
时,根据虚表中的函数地址找到的就是fish类的breathe()
函数。
对于虚函数调用来说,每个对象内部都有一个虚表指针,该虚表指针被初始化为本类的虚表,所以在程序中,不管你的对象类型如何转换,但该对象内部的虚表指针是固定的,所以才能实现动态的对象函数调用。
每个对象调用的虚函数都是通过虚表指针来索引的,在虚表指针没有初始化之前,不能调用虚函数。
总结
虚表可以继承,如果子类没有重写虚函数,那么子类虚表中仍然会有该函数的地址,只不过这个地址指向的是基类的虚函数实现,如果基类有3个虚函数,那么基类的虚表中就有三项(虚函数地址),派生类也会有虚表,至少有三项,如果重写了相应的虚函数,那么虚表中的地址就会改变,指向自身的虚函数实现。如果派生类有自己的虚函数,那么虚表中就会添加该项。
当C++编译器在编译的时候,发现animal类的
breathe()
函数是虚函数,这个时候C++就会采用迟绑定(late binding)技术。也就是编译时并不确定具体调用的函数,而是在运行时,依据对象的类型(在程序中,我们传递的fish类对象的地址)来确认调用的是哪一个函数,这种能力就叫做C++的多态性。我们没有在breathe()
函数前加virtual关键字时,C++编译器在编译时就确定了哪个函数被调用,这叫做早期绑定(early binding)。C++的多态性是通过迟绑定技术来实现的。
C++的多态性用一句话概括就是:在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数;如果对象类型是基类,就调用基类的函数。
本文参考 C++多态的实现原理。